package cn.yangliu.tools;

import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.system.oshi.OshiUtil;
import cn.yangliu.ex.NoSuchPortException;
import cn.yangliu.ex.PortOpenException;
import com.fazecast.jSerialComm.SerialPort;
import com.fazecast.jSerialComm.SerialPortDataListener;
import com.fazecast.jSerialComm.SerialPortEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.server.PortInUseException;
import oshi.software.os.OperatingSystem;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;


/**
 * @author 问道于盲
 * @date 2021-09-27
 */
@SuppressWarnings("all")
@Slf4j
public class SerialPortManager {

    /**
     * 查找所有可用端口
     *
     * @return 可用端口名称列表
     */
    public static final ArrayList<String> findPorts() {
        // 获得当前所有可用串口
        SerialPort[] serialPorts = SerialPort.getCommPorts();
        ArrayList<String> portNameList = new ArrayList<>();
        // 将可用串口名添加到List并返回该List
        if (Objects.isNull(serialPorts) || serialPorts.length == 0) {
            return portNameList;
        }
        for (SerialPort serialPort : serialPorts) {
            String systemPortName = serialPort.getSystemPortName();
            portNameList.add(systemPortName);
        }

        return portNameList;
    }

    /**
     * 打开串口
     *
     * @param portName
     *            端口名称
     * @param baudrate
     *            波特率
     * @return 串口对象
     * @throws PortInUseException
     *             串口已被占用
     */
    public static final SerialPort openPort(String portName, int baudRate, int dataBits, int stopBits, int parity) throws PortInUseException {
        OperatingSystem os = OshiUtil.getOs();
        List<String> ports = findPorts();
        if (Objects.equals(os.getFamily(), "Windows")) {
            if (!ports.contains(portName)) {
                throw new NoSuchPortException(portName);
            }
        } else {
            boolean contains = false;
            for (String port : ports) {
                if (portName.contains(port)) {
                    contains = true;
                    break;
                }
            }
            if (!contains) {
                throw new NoSuchPortException(portName);
            }
        }

        // 通过端口名识别端口
        SerialPort serialPort = SerialPort.getCommPort(portName);
        // 打开端口，并给端口名字和一个timeout（打开操作的超时时间）
        serialPort.setComPortTimeouts(2000, 2000, 2000);

        serialPort.setFlowControl(SerialPort.FLOW_CONTROL_DISABLED);
        serialPort.setComPortParameters(baudRate, dataBits, stopBits, parity);
        serialPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING | SerialPort.TIMEOUT_WRITE_BLOCKING,
                5000, 1000);
        boolean openPort = serialPort.openPort();

        if (!openPort) {
            throw new PortOpenException(portName);
        }
        return serialPort;
    }

    /**
     * 关闭串口
     *
     * @param serialport
     *            待关闭的串口对象
     */
    public static void closePort(SerialPort serialPort) {
        if (serialPort != null) {
            serialPort.closePort();
        }
    }

    /**
     * 往串口发送数据
     *
     * @param serialPort
     *            串口对象
     * @param order
     *            待发送数据
     */
    /*public static void sendToPort(SerialPort serialPort, byte[] order) {
        if (serialPort == null) {
            throw new NullPointerException("serialPort is null");
        }
        if (!serialPort.isOpen()) {
            throw new UnsupportedOperationException("serialPort is not opened");
        }
        try (OutputStream out = serialPort.getOutputStream()){
            out.write(order);
            out.flush();
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    } */

    public static void sendToPort(SerialPort serialPort, byte[] order) {
        if (serialPort == null) {
            throw new NullPointerException("serialPort is null");
        }
        if (!serialPort.isOpen()) {
            throw new UnsupportedOperationException("serialPort is not opened");
        }
        serialPort.writeBytes(order, order.length);
    }

    /**
     * 从串口读取数据
     *
     * @param serialPort
     *            当前已建立连接的SerialPort对象
     * @return 读取到的数据
     */
    public static byte[] readFromPort(SerialPort serialPort){
        byte[] bytes = {};
        //循环读取所有的返回数据。如果可读取数据长度为0或-1，则停止读取
        while (serialPort.bytesAvailable() > 0){
            //创建一个字节数组，长度为可读取的字节长度
            byte[] readBuffer = new byte[serialPort.bytesAvailable()];
            //将串口中可读取的数据读入字节数组，返回值为本次读取到的字节长度
            int numRead = serialPort.readBytes(readBuffer, readBuffer.length);
            bytes = concat(bytes, readBuffer);
            //休眠0.02秒，等待下位机传送数据到串口。如果不休眠，直接再次使用port.bytesAvailable()函数会因为下位机还没有返回数据而返回-1，并跳出循环导致数据没读完。休眠时间可以自行调试，时间越长，单次读取到的数据越多。
            ThreadUtil.sleep(20);
        }
        return bytes;
    }


    /**
     * 添加监听器
     *
     * @param port
     *            串口对象
     * @param listener
     *            串口存在有效数据监听
     */
    public static void addListener(SerialPort serialPort, StatusListener listener) {
        // 给串口添加监听器
        serialPort.addDataListener(new SerialPortListener(listener, serialPort));
    }

    /**
     * 串口监听
     */
        public static class SerialPortListener implements SerialPortDataListener {

        private StatusListener statusListener;
        private SerialPort serialPort;

        public SerialPortListener(StatusListener statusListener, SerialPort serialPort) {
            this.statusListener = statusListener;
            this.serialPort = serialPort;
        }

        @Override
        public int getListeningEvents() {
            return SerialPort.LISTENING_EVENT_DATA_AVAILABLE
                    | SerialPort.LISTENING_EVENT_CTS
                    | SerialPort.LISTENING_EVENT_DSR
                    | SerialPort.LISTENING_EVENT_RING_INDICATOR
                    | SerialPort.LISTENING_EVENT_CARRIER_DETECT;
        }

        @Override
        public void serialEvent(SerialPortEvent serialPortEvent) {
            if (Objects.isNull(statusListener)) {
                return;
            }
            switch (serialPortEvent.getEventType()) {
                case SerialPort.LISTENING_EVENT_DATA_AVAILABLE: // 1.串口存在有效数据
                    statusListener.dataAvailable();
                    break;

                case SerialPort.LISTENING_EVENT_CTS: // 3.清除待发送数据
                    statusListener.clearToSend(serialPort);
                    break;

                case SerialPort.LISTENING_EVENT_DSR: // 4.待发送数据准备好了
                    statusListener.dataSetReady(serialPort);
                    break;

                case SerialPort.LISTENING_EVENT_RING_INDICATOR: // 5.振铃指示
                    statusListener.ringIndicator(serialPort);
                    break;

                case SerialPort.LISTENING_EVENT_CARRIER_DETECT: // 6.载波检测
                    statusListener.carrierDetect(serialPort);
                    break;
                default:
                    break;
            }
        }
    }

    /**
     * 串口存在有效数据监听
     */
    public interface StatusListener {
        /**
         * 串口存在有效数据
         */
        default void dataAvailable(){}

        /**
         * 清除待发送数据
         */
        default void clearToSend(SerialPort serialPort){}

        /**
         * 待发送数据准备好了
         */
        default void dataSetReady(SerialPort serialPort){}

        /**
         * 振铃指示
         */
        default void ringIndicator(SerialPort serialPort){}

        /**
         * 载波检测
         * @param serialPortEvent serialPortEvent
         */
        default void carrierDetect(SerialPort serialPort){}
    }

    /**
     * 合并数组
     *
     * @param firstArray
     *            第一个数组
     * @param secondArray
     *            第二个数组
     * @return 合并后的数组
     */
    public static byte[] concat(byte[] firstArray, byte[] secondArray) {
        if (firstArray == null || secondArray == null) {
            return null;
        }

        byte[] bytes = new byte[firstArray.length + secondArray.length];
        if (firstArray.length != 0) {
            System.arraycopy(firstArray, 0, bytes, 0, firstArray.length);
        }
        if (secondArray.length != 0) {
            System.arraycopy(secondArray, 0, bytes, firstArray.length, secondArray.length);
        }
        return bytes;
    }
}